﻿using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;

using HorrorEncodeLib;

namespace HorrorEncodeLib {
    /// <summary>
    /// Permite codificar una cadena utilizando el algoritmo de HorrorEncode
    /// </summary>
    public static class Encode {
        /// <summary>
        /// Contiene el último Encode Map (Mapa de Codificación) utilizado.
        /// </summary>
        public static EncodeMappings LastEncodeMap {
            get;
            private set;
        }

        /// <summary>
        /// Contiene los carácteres que faltan para codificar correctamente el mensaje
        /// devuelto por el método Encode.ToString. Negativo si sobran carácteres.
        /// </summary>
        public static int RemainingChars {
            get;
            private set;
        }

        /// <summary>
        /// Codifica un Mensaje y devuelve sólo la Cadena de Bits (1 y 0) de la codificación.
        /// </summary>
        /// <param name="obj">Mensaje a codificar</param>
        /// <param name="encodeMap">Encode Map (Mapa de Codificación) a utilizar, se determina automáticamente por defecto</param>
        /// <param name="includeHeader">Si se debe incluir cabecera o no, se incluye por defecto</param>
        /// <returns>Cadena de Bits del Mensaje Codificado</returns>
        public static List<bool> ToBoolList(
            string obj,
            EncodeMappings encodeMap = EncodeMappings.SmartMapping,
            bool includeHeader = true
        ) {
            // > Validar tamaño de obj
            if ( obj.Length < 2 )
                throw new Exception("La cadena a codificar está vacía");

            // > Obtener Encode Map (Mapa de Codificación) automáticamente
            if ( encodeMap == EncodeMappings.SmartMapping )
                encodeMap = EncodeMaps.GetSmartMap(obj);

            // > Si se solicitó codificar un mensaje sin incluir cabeceras de Encode Map (Mapa de Codificación)
            //   lanzar una Excepción
            if ( !includeHeader && encodeMap == EncodeMappings.SmartMapping )
                throw new Exception("La selección de Encode Map (Mapa de Codificación) actual (SmartMapping) debe incluir cabeceras");

            // > Aquí se almacenarán los bits del mensaje codificado
            List<bool> objEncodedBits = new List<bool>();

            // > Agregar cabecera si se pidió agregarla
            if ( includeHeader )
                foreach ( bool bit in EncodeMaps.Headers[encodeMap] )
                    objEncodedBits.Add(bit);

            // > Codificar el mensaje
            foreach ( char chr in obj ) {
                try {
                    foreach ( bool bit in EncodeMaps.Maps[encodeMap][chr] )
                        objEncodedBits.Add(bit);
                } catch ( Exception ex ) {
                    throw new Exception("Carácter \"" + chr.ToString() + "\" inválido", ex);
                }
            }

            // > Establecer el último EncodeMap (Mapa de Codificación) utilizado
            LastEncodeMap = encodeMap;

            // > Devolver resultado en forma de BitArray
            return objEncodedBits;
        }

        /// <summary>
        /// Codifica un Mensaje y devuelve sólo la Cadena de Bits (1 y 0) de la codificación.
        /// </summary>
        /// <param name="obj">Mensaje a codificar</param>
        /// <param name="encodeMap">Encode Map (Mapa de Codificación) a utilizar, se determina automáticamente por defecto</param>
        /// <param name="includeHeader">Si se debe incluir cabecera o no, se incluye por defecto</param>
        /// <returns>Cadena de Bits del Mensaje Codificado</returns>
        public static BitArray ToBitArray(
            string obj,
            EncodeMappings encodeMap = EncodeMappings.SmartMapping,
            bool includeHeader = true
        ) {
            return new BitArray(ToBoolList(obj, encodeMap, includeHeader).ToArray());
        }

        /// <summary>
        /// Codifica un Mensaje y lo oculta en un Portador y devuelve el Portador con el Mensaje oculto en él.
        /// </summary>
        /// <param name="src">Mensaje a codificar</param>
        /// <param name="obj">Portador en el que se ocultará el Mensaje codificado</param>
        /// <param name="encodeMap">Encode Map (Mapa de Codificación) a utilizar, se determina automáticamente por defecto</param>
        /// <param name="includeHeader">Si se debe incluir cabecera o no, se incluye por defecto</param>
        /// <returns>Stringn con el Mensaje codificado y oculto en el Portador</returns>
        public static string ToString(
            string src,
            string obj,
            EncodeMappings encodeMap = EncodeMappings.SmartMapping,
            bool includeHeader = true
        ) {
            // > Aquí almacenaremos los Bits que representan el mensaje codificado
            BitArray encodedBits = new BitArray(0);

            // > Codificar el mensaje o devolver [null] si no fue posible
            try {
                encodedBits = ToBitArray(src, encodeMap, includeHeader);
            } catch {
                return null;
            }

            // > Inicializar algunas variables para generar el mensaje codificado
            char[] carrierChars    = obj.ToLower().ToCharArray(); // Pasar el Mensaje Portador a minúsculas en un arreglo de carácteres
            StringBuilder finalStr = new StringBuilder(); // Acá almacenamos el resultado de la codificación
            int i, thisBit; // Con esto iteramos en los mensajes

            for ( i = 0, thisBit = 0; (i < carrierChars.Length) && (thisBit < encodedBits.Length); i++ ) {
                // > Si la letra no puede usarse para codificar (no tiene diferencia de Mayúscula / Minúscula)
                //   sólo se agrega al mensaje final
                if ( !EncodeMaps.SupportedLetters.Contains(carrierChars[i].ToString()) ) {
                    finalStr.Append(carrierChars[i]);
                    continue;
                }

                // > Agregar letra Mayúscula si el bit es 1 o Minúscula si es 0
                if ( encodedBits[thisBit] )
                    finalStr.Append(carrierChars[i].ToString().ToUpper());
                else
                    finalStr.Append(carrierChars[i].ToString().ToLower());

                // Ir al siguiente bit (no se hace en cada iteración puesto que a veces no codificamos el bit)
                thisBit++;
            }

            // > Si sobran carácteres en el mensaje portador, asignarles un bit en 1 o 0 de forma aleatoria
            if ( i < carrierChars.Length )
                for ( ; i < carrierChars.Length; i++ )
                    finalStr.Append(
                        ((i * (new Random(encodedBits.Length)).Next(1, 20)) % 3 == 0)
                        ? carrierChars[i].ToString().ToLower()
                        : carrierChars[i].ToString().ToUpper()
                    );

            // > Calcular los carácteres que faltan para codificar el mensaje correctamente
            RemainingChars = encodedBits.Length - thisBit;

            // > Devolver la cadena codificada
            return finalStr.ToString();
        }
    }
}
